using System;
using System.Collections.Generic;
// This namespace contains Process
using System.Diagnostics;
// This namespace contains Color
using System.Drawing;
using System.Media;
// This namespace contains RichTextBox
using System.Windows.Forms;
using Felbot.IO;
using Felbot.Profiles;

namespace Felbot.Interface.Conversations {
    /// <summary>
    /// This is a basic class used to extend a RichTextBox, giving a programmer
    /// the ability to write chat-style events to a RichTextBox with ease.
    /// </summary>
    public class RichTextBoxEx : RichTextBox {
        /// <summary>
        /// This constructs a new RichTextBoxEx.
        /// </summary>
        public RichTextBoxEx() : base() { }

        #region Add and Edit Text
        /// <summary>This contains the text of the conversation, with the stored style, and is used for in-place style changes and style storage.</summary>
        private XmlConversation m_Conversation;

        /// <summary>This contains the last date the Timestamp was accessed, used to determine when to display a full date-time.</summary>
        private DateTime m_Last = DateTime.Today.AddDays(-1);

        private bool m_CanSearch = true;

        /// <summary>This describes the AddTextNodes threadsafe method.</summary>
        private delegate void RtbeDelegate();

        public void Initialize(string profileName) {
            m_Conversation = new XmlConversation();
            m_Conversation.Started = DateTime.Now;
            m_Conversation.ProfileName = profileName;
            m_Conversation.Lines = new List<XmlLine>();

            m_Conversation = ConversationControl.SaveConversation(m_Conversation, false);

            m_CanSearch = ProfileControl.Global.GetSetting("Interface", "RTBInLineSearch").ToBoolean(true);
            this.DetectUrls = ProfileControl.Global.GetSetting("Interface", "RTBURLDetection").ToBoolean(true);
        }

        public void Closing() {
            AddTextNodes(new TextNode(FCSObjects.Chatroom_Bot_Fail, "Closing..."));
            m_Conversation.Lines.Add(null);
            m_Conversation = ConversationControl.SaveConversation(m_Conversation, true);
        }

        /// <summary>
        /// This is a nifty little function to add a row of text to a RichTextBox.
        /// This prints a timestamp, followed by looping through all of the passed
        /// TextNodes, and then tidy's up with a NewLine (CR LF on Windows).
        /// </summary>
        /// <example>
        /// AddTextNodes(new TextNode(Color.Green, "Hello world!");
        /// [00:00:00 XM] Hello world!
        /// </example>
        /// <param name="textNodes">An array of TextNode's to be added to the RTB</param>
        public void AddTextNodes(params TextNode[] textNodes) {
            if (this.IsHandleCreated) {
                this.Invoke((RtbeDelegate) delegate() {
                    try {
                        int maxChars = 0;
                        int chars = 0;
                        if (ProfileControl.Global.GetSetting("Interface", "RTBMaxChars").ToBoolean(true))
                            maxChars = ProfileControl.Global.GetSetting("Interface", "RTBMaxCharsNumber").ToInteger(1000);
                        bool carotAtEnd = (this.SelectionStart == this.Text.Length);
                        int carotLocation = this.SelectionStart;
                        int selectionLength = this.SelectionLength;
                        this.SelectionStart = this.Text.Length;
                        this.SelectionLength = 0;
                        TextNode timeStamp;
                        if (m_Last.Equals(DateTime.Today)) {
                            timeStamp = TextNode.Timestamp;
                        } else {
                            if (m_Conversation.Lines.Count > 0)
                                Initialize(m_Conversation.ProfileName);
                            timeStamp = TextNode.FullTimestamp;
                            m_Last = DateTime.Today;
                        }
                        if (timeStamp != null) {
                            this.SelectionColor = timeStamp.Color;
                            this.SelectedText = timeStamp.Text;
                            chars += timeStamp.Text.Length;
                        }
                        this.SelectionHangingIndent = 20;
                        for (int i = 0; i < textNodes.Length; i++) {
                            if (textNodes[i] == null || textNodes[i].Text == "")
                                continue;
                            this.SelectionColor = textNodes[i].Color;
                            if (textNodes[i].Font != null)
                                this.SelectionFont = textNodes[i].Font;
                            if (maxChars > 300 && chars + textNodes[i].Text.Length > maxChars) {
                                if (chars - 3 < maxChars) {
                                    this.SelectedText = textNodes[i].Text.Substring(0, maxChars - chars - 3) + "...";
                                }
                            } else {
                                this.SelectedText = textNodes[i].Text;
                            }
                            chars += textNodes[i].Text.Length;
                        }
                        this.SelectedText = Environment.NewLine;
                        if (carotAtEnd) {
                            this.SelectionStart = this.Text.Length;
                            this.SelectionLength = 0;
                        } else {
                            this.SelectionStart = carotLocation;
                            this.SelectionLength = selectionLength;
                        }
                        this.ScrollToCaret();
                    } catch (Exception) { }
                });
            }
            XmlLine line = new XmlLine();
            line.TextNodes = new List<XmlTextNode>();
            foreach (TextNode textNode in textNodes) {
                XmlTextNode node = new XmlTextNode();
                node.Style = textNode.Style;
                node.Text = textNode.Text;
                line.TextNodes.Add(node);
            }
            line.Stamp = DateTime.Now;
            m_Conversation.Lines.Add(line);
            m_Conversation = ConversationControl.SaveConversation(m_Conversation, true);
        }

        /// <summary>
        /// Sets the RTB to have the conversation given.
        /// </summary>
        /// <param name="conversation">The XML serialized conversation object.</param>
        public void SetConversation(XmlConversation conversation) {
            m_Conversation = conversation;
            RefreshText(500);
        }

        /// <summary>
        /// Clears and rewrites the RTB with the entire stored conversation.
        /// </summary>
        public void RefreshText() {
            RefreshText(DateTime.MinValue);
        }

        /// <summary>
        /// Clears and rewrites the RTB with the entire stored conversation after the startAt value.
        /// </summary>
        /// <param name="startAt">The DateTime to start from.</param>
        public void RefreshText(DateTime startAt) {
            RefreshText(startAt, TimeSpan.MaxValue, -1);
        }

        /// <summary>
        /// Clears and rewrites the RTB with the entire stored conversation after the startAt value, with the amount of lines specified.
        /// </summary>
        /// <param name="startAt">The DateTime to start from.</param>
        /// <param name="totalLines">The total number of lines to output.</param>
        internal void RefreshText(DateTime startAt, int totalLines) {
            RefreshText(startAt, TimeSpan.MaxValue, totalLines);
        }

        /// <summary>
        /// Clears and rewrites the RTB with the entire stored conversation with the amount of lines specified.
        /// </summary>
        /// <param name="totalLines">The total number of lines to output.</param>
        public void RefreshText(int totalLines) {
            RefreshText(DateTime.MinValue, TimeSpan.MaxValue, totalLines);
        }

        /// <summary>
        /// Clears and rewrites the RTB with the entire stored conversation after the startAt value, with the length of time in the length value, and the amount of lines specified.
        /// </summary>
        /// <param name="startAt">The DateTime to start from.</param>
        /// <param name="length">The TimeSpan to show.</param>
        /// <param name="totalLines">The total number of lines to output.</param>
        public void RefreshText(DateTime startAt, TimeSpan length, int totalLines) {
            m_Last = m_Conversation.Started.AddDays(-1);
            int maxChars = 0;
            int chars = 0;
            if (ProfileControl.Global.GetSetting("Interface", "RTBMaxChars").ToBoolean(true))
                maxChars = ProfileControl.Global.GetSetting("Interface", "RTBMaxCharsNumber").ToInteger(1000);
            DateTime endAt = (length == TimeSpan.MaxValue ? DateTime.MaxValue : startAt + length);
            this.Clear();
            XmlLine line;
            if (totalLines == -1)
                totalLines = m_Conversation.Lines.Count;
            for (int i = 0; i < m_Conversation.Lines.Count; i++) {
                line = m_Conversation.Lines[i];
                if (line != null) {
                    if (i >= m_Conversation.Lines.Count - totalLines && line.Stamp >= startAt && line.Stamp <= endAt) {
                        TextNode timeStamp;
                        this.SelectionStart = this.Text.Length;
                        this.SelectionLength = 0;
                        if (m_Last.Date.Equals(line.Stamp.Date)) {
                            timeStamp = new TextNode(FCSObjects.Chatroom_Timestamp, line.Stamp.ToLongTimeString());
                        } else {
                            timeStamp = new TextNode(FCSObjects.Chatroom_Timestamp, line.Stamp.ToLongDateString() + "  " + line.Stamp.ToLongTimeString());
                            m_Last = line.Stamp.Date;
                        }
                        if (timeStamp != null) {
                            this.SelectionColor = timeStamp.Color;
                            this.SelectedText = timeStamp.Text;
                            chars += timeStamp.Text.Length;
                        }
                        this.SelectionHangingIndent = 20;
                        for (int j = 0; j < line.TextNodes.Count; j++) {
                            if (line.TextNodes[j] == null || line.TextNodes[j].Text == "")
                                continue;
                            int styleInt = (int) FCSObjects.Chatroom_Text;
                            TextNode modelNode;
                            if (int.TryParse(line.TextNodes[j].Style, out styleInt)) {
                                modelNode = FCSControl.GetTextNode((FCSObjects) styleInt, line.TextNodes[j].Text);
                            } else {
                                FCSSetting setting = FCSSetting.FromString(line.TextNodes[j].Style);
                                modelNode = FCSControl.GetFromFCSSetting(setting, new FCSSetting(null, FontStyle.Regular, Color.Empty, Color.Empty, FCSStyle.Default, "%t"), line.TextNodes[j].Text);
                            }
                            this.SelectionColor = modelNode.Color;
                            if (modelNode.Font != null)
                                this.SelectionFont = modelNode.Font;
                            if (maxChars > 300 && chars + modelNode.Text.Length > maxChars) {
                                if (chars < maxChars) {
                                    this.SelectedText = modelNode.Text.Substring(0, maxChars - chars - 3) + "...";
                                }
                            } else {
                                this.SelectedText = modelNode.Text;
                            }
                            chars += modelNode.Text.Length;
                        }
                    }
                }
                if (this.Text.Length > 0) //only if the newline is not at the beginning
                    this.SelectedText = Environment.NewLine;
                chars = 0;
            }
            this.SelectionStart = this.Text.Length;
            this.SelectionLength = 0;
            this.ScrollToCaret();
        }
        #endregion

        #region Keep Carot in View
        protected override void OnResize(EventArgs e) {
            base.OnResize(e);
            this.ScrollToCaret();
        }
        #endregion

        #region In-Line Search
        /// <summary>This stores the current in-line search parameter.</summary>
        private string m_Search = null;
        /// <summary>This is the box used to show the search item.</summary>
        private TextBox m_txtSearch;

        /// <summary>This describes the MouseMove and MouseClick event used by the in-line search feature.</summary>
        private delegate void MouseEventDelegate(object sender, MouseEventArgs e);

        /// <summary>
        /// This is overridden to capture textual keystrokes and route them to the in-line search box.
        /// </summary>
        protected override void OnKeyPress(KeyPressEventArgs e) {
            string key = e.KeyChar.ToString();
            if (m_CanSearch) {
                UpdateSearch(m_Search + key);
                e.Handled = true;
            } else {
                //todo: move char to sendbox if possible
            }
        }

        /// <summary>
        /// This is overridden to capture the Tab, Shift+Tab, Backspaace, and Escape keys used by the inline search feature.
        /// </summary>
        protected override bool ProcessCmdKey(ref Message m, Keys keyData) {
            if (keyData == (Keys.Tab | Keys.Shift)) {
                if (m_Search != null) {
                    Search_Previous();
                    return true;
                }
            } else if (keyData == Keys.Tab) {
                if (m_Search != null) {
                    SearchNext(true, true);
                    return true;
                }
            } else if (keyData == Keys.Back) {
                if (m_Search == null || m_Search.Length <= 1)
                    UpdateSearch(null);
                else
                    UpdateSearch(m_Search.Substring(0, m_Search.Length - 1));
                return true;
            } else if (keyData == Keys.Escape) {
                UpdateSearch(null);
                return true;
            } else if (keyData == (Keys.Control | Keys.X) || keyData == (Keys.Control | Keys.V) || keyData == Keys.Delete) {
                return true;
            } else if (keyData == (Keys.Control | Keys.C)) {
                this.Copy();
                return true;
            }

            return base.ProcessCmdKey(ref m, keyData);
        }

        /// <summary>
        /// This updates the search field and changes the selection if nessicary.
        /// </summary>
        /// <param name="newsearch"></param>
        private void UpdateSearch(string newSearch) {
            if (!m_CanSearch)
                return;
            if (m_Search == null && newSearch != null) {
                m_txtSearch = new TextBox();
                if (this.Bounds.Contains(Parent.PointToClient(Control.MousePosition)))
                    m_txtSearch.Location = Parent.PointToClient(Control.MousePosition) + new Size(10, 10);
                else
                    m_txtSearch.Location = Parent.PointToClient(this.PointToScreen(this.GetPositionFromCharIndex(this.SelectionStart))) + new Size(10, 10);
                m_txtSearch.Size = new Size(60, 21);
                m_txtSearch.ReadOnly = true;
                m_txtSearch.ForeColor = Color.Black;
                m_txtSearch.BackColor = Color.White;
                m_txtSearch.BorderStyle = BorderStyle.Fixed3D;
                Parent.Controls.Add(m_txtSearch);
                m_txtSearch.BringToFront();
                MouseEventDelegate mouse_delegate = (MouseEventDelegate) delegate(object sender, MouseEventArgs e) {
                        m_txtSearch.Dispose();
                        m_txtSearch = null;
                        m_Search = null;
                        this.SelectionLength = 0;
                    };
                m_txtSearch.MouseMove += new MouseEventHandler(mouse_delegate);
                m_txtSearch.MouseClick += new MouseEventHandler(mouse_delegate);
                m_txtSearch.Show();
            } else if (newSearch == null) {
                if (m_txtSearch != null) {
                    m_txtSearch.Dispose();
                    m_txtSearch = null;
                }
                m_Search = null;
                this.SelectionLength = 0;
                return;
            } else if (m_Search.StartsWith(newSearch)) { //backspace
                m_txtSearch.Text = newSearch;
                m_Search = newSearch;

                SearchNext(false, false);
                return;
            }

            m_txtSearch.Text = newSearch;
            m_Search = newSearch;

            SearchNext(true, false);
        }

        private void SearchNext(bool ding, bool forceAdvance) {
            int found = this.Find(m_Search, this.SelectionStart + (forceAdvance ? 1 : 0), this.Text.Length, RichTextBoxFinds.None);
            if (found == -1) {
                if (m_Search.StartsWith(this.SelectedText, StringComparison.CurrentCultureIgnoreCase)) {
                    found = this.Find(m_Search, 0, this.SelectionStart - 1, RichTextBoxFinds.None);
                    if (found == -1) {
                        m_txtSearch.BackColor = Color.Salmon;
                        if (ding)
                            SystemSounds.Exclamation.Play();
                    } else {
                        m_txtSearch.BackColor = Color.PaleGreen;
                        this.SelectionStart = found;
                        this.SelectionLength = m_Search.Length;
                        this.ScrollToCaret();
                    }
                } else {
                    m_txtSearch.BackColor = Color.Salmon;
                    if (ding)
                        SystemSounds.Exclamation.Play();
                }
            } else {
                m_txtSearch.BackColor = Color.PaleGreen;
                this.SelectionStart = found;
                this.SelectionLength = m_Search.Length;
                this.ScrollToCaret();
            }
        }

        private void Search_Previous() {
            int found = this.Find(m_Search, 0, this.SelectionStart - 1, RichTextBoxFinds.Reverse);
            if (found == -1) {
                if (m_Search.StartsWith(this.SelectedText, StringComparison.CurrentCultureIgnoreCase)) {
                    found = this.Find(m_Search, this.SelectionStart + 1, this.Text.Length, RichTextBoxFinds.Reverse);
                    if (found == -1) {
                        m_txtSearch.BackColor = Color.Salmon;
                        SystemSounds.Exclamation.Play();
                    } else {
                        m_txtSearch.BackColor = Color.PaleGreen;
                        this.SelectionStart = found;
                        this.SelectionLength = m_Search.Length;
                        this.ScrollToCaret();
                    }
                } else {
                    m_txtSearch.BackColor = Color.Salmon;
                    SystemSounds.Exclamation.Play();
                }
            } else {
                m_txtSearch.BackColor = Color.PaleGreen;
                this.SelectionStart = found;
                this.SelectionLength = m_Search.Length;
                this.ScrollToCaret();
            }
        }
        #endregion

        #region Link Clicking
        /// <summary>
        /// Called when a link is clicked. Sends the user to the link clicked.
        /// </summary>
        /// <param name="e">The event args.</param>
        protected override void OnLinkClicked(LinkClickedEventArgs e) {
            try {
                Process.Start(e.LinkText);
                this.SelectionStart = this.Text.Length;
                this.SelectionLength = 0;
                this.ScrollToCaret();
            } catch (Exception) { }

            base.OnLinkClicked(e);
        }
        #endregion
    }

    /// <summary>
    /// This class is a simple get/set to hold a color and a string, which is used
    /// for passing to the RichTextBoxEx class, for displaying formatted text easily
    /// in a RichTextBox. Also, this has the static method Timestamp, which returns
    /// a TextNode instance with a timestamp in white text.
    /// 
    /// This class was designed with the intention of constructing it in-line of
    /// a call to a function that uses it, therefore it should only exist for
    /// the extent of the function using it. To conform with that intention,
    /// I decided to make this class immutable -- that is, once you pass the
    /// arguments to it's constructor, it's data can not be changed.
    /// </summary>
    /// <example>
    /// rtb.AddTextNodes(new ChatNode(Color.Green, "Success!"));
    /// </example>
    public class TextNode {
        private Font m_Font = null;
        private Color m_Color = Color.Empty;
        private string m_StyleText;
        private string m_Text = "";

        /// <summary>This is the color that the string should be displayed in.</summary>
        public Color Color {
            get {
                return m_Color;
            }
        }

        /// <summary>This is the string to be displayed.</summary>
        public string Text {
            get {
                return m_Text;
            }
        }

        /// <summary>This is the font to be displayed. If null, uses RTB's current font.</summary>
        public Font Font {
            get {
                return m_Font;
            }
        }

        /// <summary>This is the raw style string to be displayed.</summary>
        public string Style {
            get {
                return m_StyleText;
            }
        }

        /// <summary>
        /// This is the standard constructor for a TextNode, allowing it's caller
        /// to set the color and text that it refers to. There isn't a no-args
        /// constructor, as that would possibly cause issues with displaying
        /// "null" colors and strings.
        /// </summary>
        /// <param name="c">The <c>System.Drawing.Color to use</c></param>
        /// <param name="s">A <c>String</c> of text to display</param>
        public TextNode(Color color, string text)
            : this(null, color, text) { }

        /// <summary>
        /// This is the standard constructor for a TextNode, allowing it's caller
        /// to set the color and text that it refers to. There isn't a no-args
        /// constructor, as that would possibly cause issues with displaying
        /// "null" colors and strings. New! Now with Fonts selectable.
        /// </summary>
        /// <param name="f">The <c>System.Drawing.Font</c> to use.</param>
        /// <param name="c">The <c>System.Drawing.Color</c> to use.</param>
        /// <param name="s">A <c>String</c> of text to display</param>
        public TextNode(Font font, Color color, string text) {
            if (font == null)
                m_StyleText = new FCSSetting(null, FontStyle.Regular, color, Color.Empty, FCSStyle.Default, null).ToString();
            else
                m_StyleText = new FCSSetting(font.FontFamily.Name, font.Style, color, Color.Empty, FCSStyle.Default, null).ToString();
            m_Font = font;
            m_Color = color;
            m_Text = text.Replace("\0", "");
        }

        /// <summary>
        /// This is the standard constructor for a TextNode, allowing it's caller
        /// to set the color and text that it refers to. There isn't a no-args
        /// constructor, as that would possibly cause issues with displaying
        /// "null" colors and strings. New! Now with Fonts selectable.
        /// </summary>
        /// <param name="f">The <c>System.Drawing.Font</c> to use.</param>
        /// <param name="c">The <c>System.Drawing.Color</c> to use.</param>
        /// <param name="s">A <c>String</c> of text to display</param>
        public TextNode(FCSObjects style, string text) {
            TextNode tn = FCSControl.GetTextNode(style, text);
            m_StyleText = ((int) style).ToString();
            m_Font = tn.Font;
            m_Color = tn.Color;
            m_Text = tn.Text.Replace("\0", "");
        }

        /// <summary>Gets a TextNode timestamp</summary>
        public static TextNode Timestamp {
            get {
                return new TextNode(FCSObjects.Chatroom_Timestamp, DateTime.Now.ToLongTimeString());
            }
        }

        /// <summary>Gets a TextNode full timestamp (with date)</summary>
        public static TextNode FullTimestamp {
            get {
                return new TextNode(FCSObjects.Chatroom_Timestamp, DateTime.Now.ToLongDateString() + "  " + DateTime.Now.ToLongTimeString());
            }
        }

        public override string ToString() {
            return "{" + Color.Name + ": " + Text + "}";
        }
    }
}